<template>
  <div class="sw-pics-uploader">
    <div
      ref="$uploadContentContainer"
      class="sw-pics-uploader__content"
      :class="props.draggable ? 'sw-pics-uploader__content--draggable' : ''"
    >
      <template v-for="(element, index) in props.modelValue" :key="index">
        <div class="sw-pics-uploader__item" @click.stop="">
          <div class="sw-pics-uploader-item-preview">
            <slot :data="element" :index="index">
              <img
                class="sw-pics-uploader-item-preview__pic"
                v-lazy="
                  element.tempFilePath ||
                  addBaseUrl(element.data.path, props.basePrefix || '')
                "
                v-if="!element.error"
              />
              <div class="sw-pics-uploader-item-preview__err" v-else>
                <div class="sw-pics-uploader-item-preview__err__icon">!</div>
                <div class="sw-pics-uploader-item-preview__err__test">
                  {{ element.errMsg }}
                </div>
              </div>
              <div
                class="sw-pics-uploader-item-preview__progress"
                v-if="element.loading"
              >
                <div
                  class="sw-pics-uploader-item-preview__progress__stage"
                  :style="{ transform: `scaleX(${element.progress})` }"
                ></div>
              </div>
            </slot>
          </div>
          <div class="sw-pics-uploader__item__remove">
            <el-icon
              class="sw-pics-uploader__item__remove__icon"
              color="#fff"
              size="15px"
              @click.stop="handleDelete(index, element)"
              ><Close />
            </el-icon>
          </div>
        </div>
      </template>
      <template v-if="props.modelValue.length < props.limit">
        <el-upload
          action="#"
          :auto-upload="false"
          :show-file-list="false"
          :disabled="disabled"
          :accept="props.accept"
          :on-change="handleSelectChange"
          :on-exceed="handleSelectExceed"
          :limit="props.limit"
          :multiple="props.multiple"
          class="sw-pics-uploader__add"
        >
          <slot name="upload">
            <div :class="disabled ? 'sw-pics-uploader__add--disabled' : ''">
              <el-icon><Plus /></el-icon>
            </div>
          </slot>
        </el-upload>
      </template>
    </div>
    <div class="sw-pics-uploader-tip" v-if="props.tip">
      <slot name="tip">
        {{ props.placeholder }} <span>{{ tipStr }}</span>
      </slot>
    </div>
    <el-dialog
      v-model="cropDisplay"
      title="选择图片截取位置"
      width="800px"
      destroy-on-close
      append-to-body
      align-center
      :close-on-click-modal="false"
      :close-on-press-escape="false"
    >
      <Crop
        :crop-image-file="cropImageFile"
        @on-crop="handleCrop"
        @on-cancel="handleCropCancel"
        @on-error="handleCropError"
        :specs-height-px="props.specsHeightPx"
        :specs-width-px="props.specsWidthPx"
      ></Crop>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import {
  ElMessage,
  ElMessageBox,
  UploadFile,
  ElDialog,
  ElUpload,
  ElIcon,
} from 'element-plus'
import {
  computed,
  defineAsyncComponent,
  getCurrentInstance,
  onMounted,
  onUnmounted,
  reactive,
  ref,
  watch,
  useSlots,
  nextTick,
} from 'vue'
import { Plus, Close } from '@element-plus/icons-vue'
import { uploadFile as _uploadFile } from './utils/uploadFile'
import { addBaseUrl } from './utils/addBaseUrl'
import { TipConfig, UpladRequestConfig, UploadDataItem } from './interface'
import Sortable from 'sortablejs'
const Crop = defineAsyncComponent(() => import('./SwUploadImageCrop.vue'))

const props = withDefaults(
  defineProps<{
    modelValue: UploadDataItem[]
    accept?: string
    disabled?: boolean
    limit?: number
    multiple?: boolean
    size?: number //最大限制，单位M
    placeholder?: string //
    tip?: boolean
    tipFormat?: (tipConfig: TipConfig) => string //提示文案格式化
    specsWidthPx?: number //裁剪宽度
    specsHeightPx?: number //裁剪高度
    crop?: boolean //是否启用裁剪，裁剪模式下仅允许单个上传
    skipError?: boolean //是否跳过验证错误的图片（包括图片大小、图片类型），当设置false时，会将错误占用一个图片位置并显示对应错误信息
    draggable?: boolean //是否允许拖拽调整顺序
    uploadRequestConfig?: UpladRequestConfig // 上传配置
    basePrefix?: string // 图片前缀
    uploadDataFormat?: (file: File) => unknown | Promise<unknown> // 上传数据格式化
    uploadResponseFormat?: (
      res: any
    ) =>
      | { [p: string]: any; path: string }
      | Promise<{ [p: string]: any; path: string }> // 上传响应格式化
  }>(),
  {
    modelValue: () => [],
    disabled: false,
    accept: [
      'image/png',
      'image/jpeg',
      'image/jpg',
      'image/webp',
      'image/bmp',
    ].join(','),
    limit: Number.MAX_VALUE,
    multiple: true,
    size: 20,
    tip: true,
    placeholder: '上传图片',
    specsWidthPx: 600,
    specsHeightPx: 336,
    specs: false,
    skipError: false,
    draggable: false,
    uploadResponseFormat: (res: any) => ({
      path: res.data.data.name,
    }),
  }
)
const emits = defineEmits<{
  (e: 'update:modelValue', val: { data: { path: string } }[]): void
  (e: 'on-change', val?: any): void
  (e: 'on-exceed', val?: any): void
  (e: 'on-success', val?: any): void
  (e: 'on-error', val?: any): void
}>()

const $uploadContentContainer = ref<HTMLDivElement>()

let sortable: Sortable

const uploadDisabled = ref(false)
const cropDisplay = ref(false)
const cropImageFile = ref<File | undefined | null>(null) //待裁剪的图片文件
const tipStr = computed(() => {
  if (props.tipFormat) {
    const _tipConfig: TipConfig = {
      accept: props.accept,
      disabled: props.disabled,
      limit: props.limit,
      multiple: props.multiple,
      size: props.size,
      placeholder: props.placeholder,
      tip: props.tip,
      specsWidthPx: props.specsWidthPx,
      specsHeightPx: props.specsHeightPx,
      crop: props.crop,
    }
    return props.tipFormat(_tipConfig)
  }
  const typeMap: { [p: string]: string } = {
    'image/png': 'png',
    'image/jpeg': 'jpeg',
    'image/jpg': 'jpg',
    'image/webp': 'webp',
    'image/bmp': 'bmp',
  }
  let _typeText = ''
  let _sizeText = ''
  let { specsWidthPx, specsHeightPx, crop } = props
  let _whText =
    crop && specsHeightPx && specsWidthPx
      ? `不低于${specsWidthPx}x${specsHeightPx}像素的`
      : ''
  if (props.accept)
    _typeText = `上传${_whText}${props.accept
      .split(',')
      .map((el) => typeMap[el])
      .join('、')}类型的图片，且`
  _sizeText = props.size + 'M'
  return '| ' + _typeText + `单个文件不得超过${_sizeText}`
})
const disabled = computed(
  () =>
    props.disabled ||
    uploadDisabled.value ||
    props.modelValue.filter((el) => !el.isAdd).length == props.limit
)
const sizeM = 1024 * 1024

// 点击上传时获取图片文件
const handleSelectChange = async (uploadFile: UploadFile) => {
  if (props.modelValue.filter((el) => !el.isAdd).length >= props.limit) return
  let fileType = uploadFile.raw?.type || ''
  let size = uploadFile.raw?.size || 0
  let errMsg = ''
  if (props.accept && props.accept.split(',').indexOf(fileType) == -1) {
    ElMessage.warning('不符合上传文件类型')
    errMsg = '不符合上传文件类型'
    if (props.skipError) return
  } else if (size > props.size * sizeM) {
    ElMessage.warning(`最大不得超过${props.size}M`)
    errMsg = `请上传小于${props.size}M的图片`
    if (props.skipError) return
  }
  // 存在错误则不进入裁剪
  if (!errMsg && props.crop && props.specsHeightPx && props.specsWidthPx) {
    openCropImage(uploadFile.raw)
    return
  }
  let file = uploadFile.raw
  let name = uploadFile.name
  if (file == undefined) return
  const uploadData = (await props.uploadDataFormat?.(file)) || {
    name,
    file,
  }

  const _uploadItem: UploadDataItem = reactive({
    data: { path: '' },
    fileName: name,
    tempFilePath: URL.createObjectURL(file),
    loading: !errMsg, //存在错误则不会上传
    errMsg,
    progress: 0,
  })
  props.modelValue.push(_uploadItem)
  if (!props.skipError && errMsg) return //存在错误不会继续上传
  uploadDisabled.value = true
  _uploadFile({
    data: uploadData,
    onUploadProgress: (progressEvent) => {
      let _progress = progressEvent.progress
      _uploadItem.progress = _progress || 0
    },
    requestConfig: props.uploadRequestConfig,
  })
    .then(async (res) => {
      let _data = (await props.uploadResponseFormat?.(res)) || res.data.data
      _uploadItem.loading = false
      _uploadItem.success = true
      uploadDisabled.value = false
      Object.assign(_uploadItem, { data: _data })
      emits('on-change', _uploadItem)
      emits('on-success', _uploadItem)
      ElMessage.success('上传成功')
    })
    .catch(() => {
      _uploadItem.loading = false
      _uploadItem.error = true
      _uploadItem.errMsg = '上传失败'
      uploadDisabled.value = false
      emits('on-error', _uploadItem)
      ElMessage.error('上传失败')
    })
}

// 覆盖上传
const handleSelectExceed = async (files: File[]) => {
  emits('on-exceed', files)
  // let _files = files.slice(0, props.limit - props.modelValue.length)//获取可上传长度
  let _files: Array<File & { errMsg?: string }> = [] //实际上传的文件
  files.forEach((el) => {
    // 达到长度则退出
    if (
      _files.length >=
      props.limit - props.modelValue.filter((el) => !el.isAdd).length
    )
      return
    // 符合文件类型
    let { type: fileType, name, size } = el
    let errMsg = ''
    if (props.accept && props.accept.split(',').indexOf(fileType) >= 0) {
      if (size > props.size * sizeM) {
        ElMessage.warning(`最大不得超过${props.size}M`)
        errMsg = `请上传小于${props.size}M的图片`
        if (props.skipError) return
      }
      _files.push(Object.assign(el, { errMsg }))
    } else {
      ElMessage.warning(`${name} 不符合上传类型`)
      errMsg = '不符合上传文件类型'
      if (props.skipError) return
    }
  })

  if (_files.length > 0) {
    if (props.crop && props.specsHeightPx && props.specsWidthPx) {
      openCropImage(_files[0])
      return
    }
    uploadDisabled.value = true
    //防止单个上传导致后续上传数组长度为零无法触发恢复禁用
  }
  for (let _file of _files) {
    let _uploadItem: UploadDataItem = reactive({
      data: { path: '' },
      fileName: _file.name,
      tempFilePath: URL.createObjectURL(_file),
      loading: !_file.errMsg,
      errMsg: _file.errMsg,
      progress: 0,
    })
    if (props.modelValue.length >= props.limit) return

    props.modelValue.push(_uploadItem)

    if (!props.skipError && _file.errMsg) {
      uploadDisabled.value = false
      return
    } //存在错误不会继续上传
    const _uploadDatat = (await props.uploadDataFormat?.(_file)) || {
      name: _file.name,
      file: _file,
    }

    _uploadFile({
      data: _uploadDatat,
      onUploadProgress: (progressEvent) => {
        let _progress = progressEvent.progress
        _uploadItem.progress = _progress || 0
      },
      requestConfig: props.uploadRequestConfig,
    })
      .then(async (res) => {
        let _data = (await props.uploadResponseFormat?.(res)) || res.data.data
        _uploadItem.loading = false
        _uploadItem.success = true
        Object.assign(_uploadItem, { data: _data })
        uploadDisabled.value = false
        emits('on-change', _uploadItem)
        emits('on-success', _uploadItem)
        ElMessage.success('上传成功')
      })
      .catch(() => {
        _uploadItem.loading = false
        _uploadItem.error = true
        _uploadItem.errMsg = '上传失败'
        uploadDisabled.value = false
        emits('on-error', _uploadItem)
        ElMessage.error('上传失败')
      })
  }
}

/** 裁剪图片，选择图片后调用 */
const openCropImage = (uploadFile?: File) => {
  if (props.multiple || !props.crop) return
  cropImageFile.value = uploadFile
  cropDisplay.value = true
}
// 截取确认
const handleCrop = async (
  uploadFile: File | false,
  done: Function,
  loading: Function
) => {
  if (uploadFile) {
    // 同图片上传
    let name = uploadFile.name
    uploadDisabled.value = true

    const _uploadItem: UploadDataItem = reactive({
      data: { path: '' },
      fileName: name,
      tempFilePath: URL.createObjectURL(uploadFile),
      loading: true,
    })
    props.modelValue.push(_uploadItem)
    const _uploadData = (await props.uploadDataFormat?.(uploadFile)) || {
      name,
      file: uploadFile,
    }

    _uploadFile({
      data: _uploadData,
      requestConfig: props.uploadRequestConfig,
      onUploadProgress: (progressEvent) => {
        let _progress = progressEvent.progress
        _uploadItem.progress = _progress || 0
      },
    })
      .then(async (res) => {
        let _data = (await props.uploadResponseFormat?.(res)) || res.data.data
        _uploadItem.loading = false
        _uploadItem.success = true
        Object.assign(_uploadItem, { data: _data })
        uploadDisabled.value = false
        emits('on-change', _uploadItem)
        emits('on-success', _uploadItem)
        ElMessage.success('上传成功')
        cropImageFile.value = null
        done()
      })
      .catch(() => {
        _uploadItem.loading = false
        _uploadItem.error = true
        _uploadItem.errMsg = '上传失败'
        uploadDisabled.value = false
        emits('on-error', _uploadItem)
        ElMessage.error('上传失败')
        loading()
      })
  }
}
const handleCropCancel = () => {
  cropDisplay.value = false
  cropImageFile.value = null
}
const handleCropError = (error: any) => {
  cropDisplay.value = false
  cropImageFile.value = null
  error && ElMessage.warning(error)
}

const handleDelete = (index: number, item: UploadDataItem) => {
  if (item.error) {
    props.modelValue.splice(index, 1)
    return
  }
  ElMessageBox.confirm('是否确认删除该图片？', '提示', {
    type: 'warning',
    confirmButtonText: '确认',
    cancelButtonText: '取消',
  })
    .then(() => {
      ElMessage.success('删除成功')
      props.modelValue.splice(index, 1)
      emits('update:modelValue', props.modelValue)
      emits('on-change', props.modelValue)
    })
    .catch(() => {})
}

let g: HTMLElement | null = null
const realList = computed(() => props.modelValue)
// 当前拖拽的上下文（原数据 和 原索引）
const context = ref<{ element: UploadDataItem; index: number } | null>(null)
const visibleIndexes = ref<number[]>([])
const getChildrenNodes = () => {
  return Array.from($uploadContentContainer.value?.children || [])
}
const getVmIndex = (e?: number = -1) => {
  const _indexs = visibleIndexes.value
  const _len = _indexs.length
  return e > _len - 1 ? _len : _indexs[e]
}
const getUnderlyingVm = (item: HTMLElement) => {
  let _index = (getChildrenNodes() || []).map((el) => el).indexOf(item)
  return -1 === _index
    ? null
    : { index: _index, element: realList.value[_index] }
}
const updatePosition = (oldIndex: number, newIndex: number) => {
  props.modelValue.splice(newIndex, 0, props.modelValue.splice(oldIndex, 1)[0])
}

const i = <T>(e: T[], t: number | null = null) => {
  const r: T[] = []
  if (t == null || t > e.length) {
    t = e.length
  }
  for (let n = 0, r = new Array(t); n < t; n++) {
    r[n] = e[n]
  }
  return r
}
// 将非数组对象转换为数组
const s = (e: any) => {
  if (Array.isArray(e)) return i(e)
  if ('undefined' != typeof Symbol && Symbol.iterator in Object(e))
    return Array.from(e)
  throw new TypeError(
    'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.'
  )
}

const c = {
  c: (e: HTMLElement, t: HTMLElement, n: number) => {
    let r = 0 === n ? e.children[0] : e.children[n - 1].nextSibling
    e.insertBefore(t, r)
  },
  d: (e: HTMLElement) => {
    null !== e.parentElement && e.parentElement.removeChild(e)
  },
}

const computeIndexes = () => {
  nextTick(() => {
    visibleIndexes.value = ((e, t) => {
      if (!e) return []
      const i: Element[] = e.map((e) => e) // 拖拽容器内的所有子节点
      const a: number = t?.length || -1 // 拖拽容器内的子节点数量
      const o: number[] = s(t).map((e, t) => (t >= a ? i.length : i.indexOf(e))) // 拖拽容器内的子节点索引
      return o
    })(getChildrenNodes(), $uploadContentContainer.value?.children)
  })
}

const onDragStart = (e: Sortable.SortableEvent) => {
  context.value = getUnderlyingVm(e.item)
  g = e.item
}
const onDragUpdate = (e: Sortable.SortableEvent) => {
  Object(c.d)(e.item)
  Object(c.c)(e.from, e.item, e.oldIndex)
  let _oldIndex = context.value?.index
  _oldIndex = _oldIndex == undefined ? -1 : _oldIndex
  const _newIndex = getVmIndex(e.newIndex)
  updatePosition(_oldIndex, _newIndex)
}

const onDragEnd = () => {
  computeIndexes()
  g = null
}

watch(
  () => props.modelValue,
  (newVal) => {}
)
onMounted(() => {
  if (props.crop && props.multiple)
    console.error('Please disable multi-selection in crop mode.')
  if ($uploadContentContainer.value) {
    sortable = new Sortable($uploadContentContainer.value, {
      group: {
        name: 'uploadContentContainer', //组名
        pull: false, //禁止移出
        put: false, //禁止移入
      },
      sort: true, //进行排序
      delay: 0, //鼠标选中开始拖拽延迟时间
      disabled: !props.draggable, //是否禁用拖拽
      animation: 500, //排序动画时间
      easing: 'cubic-bezier(0.23, 1, 0.320, 1)', //排序动画缓动
      handle: '.sw-pics-uploader__item', //拖拽元素类选择器手柄，只有拖拽手柄才能拖拽
      filter: '.sw-pics-uploader__add', //过滤元素类选择器
      preventOnFilter: true, //是否禁止在过滤时进行排序
      draggable: '.sw-pics-uploader__item', //拖拽元素类选择器
      ghostClass: 'sortable-ghost', //drop placeholder的css类名
      chosenClass: 'sortable-chosen', // 被选中项的css 类名
      dragClass: 'sortable-drag', // 正在被拖拽中的css类名
      // dataIdAttr: 'data-id', // 数据id属性名

      swapThreshold: 0.65, // 拖拽元素与目标元素交换的阈值
      invertSwap: false, // Will always use inverted swap zone if set to true
      invertedSwapThreshold: 1, // Threshold of the inverted swap zone (will be set to swapThreshold value by default)
      direction: 'horizontal', //排序方向（默认自动判断方向）

      forceFallback: false, //忽略 HTML5拖拽行为，强制回调进行
      fallbackClass: 'sortable-fallback', //当使用forceFallback的时候，被复制的dom的css类名
      fallbackOnBody: false, //将cloned DOM 元素挂到body元素上。
      fallbackTolerance: 0, // Specify in pixels how far the mouse should move before it's considered as a drag.

      scroll: true, //是否开启滚动
      // scrollFn: (/**Event*/) => {
      //   console.log('scroll')
      // },
      scrollSensitivity: 30, // 滚动灵敏度
      scrollSpeed: 10, // 滚动速度
      bubbleScroll: true, //  apply autoscroll to all parent elements, allowing for easier movement

      dragoverBubble: false, // 是否开启拖拽气泡
      removeCloneOnHide: true, // Remove the clone element when it is not showing, rather than just hiding it
      emptyInsertThreshold: 5, //  px, distance mouse must be from empty sortable to insert drag element into it
      onStart: onDragStart,
      onUpdate: onDragUpdate,
      onEnd: onDragEnd,
    })
  }
  computeIndexes()
})
onUnmounted(() => {
  sortable?.destroy()
})
</script>
<style lang="scss" scoped>
@use './bem.scss' as *;

@include b(pics-uploader) {
  width: 100%;
  @include e(content) {
    display: flex;
    flex-wrap: wrap;
    @include m(draggable) {
      & > .sw-pics-uploader__item {
        cursor: move;
      }
    }
  }
  @include e(item) {
    width: 120px;
    height: 120px;
    box-sizing: border-box;
    margin: 0 10px 10px 0;
    user-select: none;
    position: relative;
    border-radius: 4px;
    overflow: hidden;
    @include e(remove) {
      position: absolute;
      right: 3px;
      top: 3px;
      width: 22px;
      height: 22px;
      background-color: rgba(0, 0, 0, 0.65);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 3;
      cursor: pointer;
    }
  }
  @include e(add) {
    width: 120px;
    height: 120px;
    box-sizing: border-box;
    margin: 0 10px 10px 0;
    user-select: none;
    position: relative;
    border-radius: 4px;
    border: 1px dashed #777;
    &:hover {
      border-color: #409eff;
    }
    @include m(disabled) {
      background-color: #fafafa;
      color: #777;
      &:hover.upload-content {
        border-color: #777;
      }
    }
  }
}

@include b(pics-uploader-item-preview) {
  width: 100%;
  height: 100%;
  background-color: #f4f5f9;
  @include e(pic) {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  @include e(err) {
    cursor: default !important;
    background-color: #e3e5e7;
    text-align: center;
    color: #9499a0;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    height: 100%;
    width: 100%;
    @include e(icon) {
      border: 1px solid #9499a0;
      border-radius: 50%;
      width: 20px;
      height: 20px;
      text-align: center;
      line-height: 20px;
      margin-bottom: 2px;
    }
    @include e(text) {
      width: 80%;
      display: -webkit-box;
      text-overflow: ellipsis;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      font-size: 14px;
    }
  }
  @include e(progress) {
    position: absolute;
    bottom: 6px;
    left: 50%;
    transform: translateX(-50%);
    width: 80%;
    height: 8px;
    background-color: rgba(29, 29, 29, 0.6);
    border-radius: 4px;
    overflow: hidden;
    @include e(stage) {
      width: 100%;
      height: 100%;
      background-color: #95d475;
      transition: width 1s linear;
      transform: scaleX(0);
      transform-origin: 0 0;
    }
  }
}

@include b(pics-uploader-tip) {
  font-size: 12px;
  color: #999;
  user-select: none;
}

:deep(.el-upload) {
  width: 100%;
  height: 100%;
}
</style>
